# -*- coding: utf-8 -*-

"""
Module about timing classes and helpers.

"""

__author__ = "dr0iddr0id {at} gmail [dot] com (C) 2010"

import sys

import logging
_LOGGER = logging.getLogger('pyknic.timing')

# ------------------------------------------------------------------------------
if __debug__:
    _LOGGER.debug('%s loading ... \n' % (__name__))
    import time
    _START_TIME = time.time()
# ------------------------------------------------------------------------------

import warnings
import heapq
from heapq import heappop, heappush


from . import events


# ------------------------------------------------------------------------------

class GameTime(object):
    """
    GameTime allows you to manipulate the virtual time in the game. You can
    slow down or speed up the time in your game. Also you can pause any time
    dependent code by setting the dilatation factor to 0. Tying to set a
    negative factor value will raise a NegativeFactorException.
    Each call to update will fire the (public) 'event_update' with the
    arguments: gdt
    (GameTime instance, game delta time, game time, real delta time, real time)

    Through the attribute time the current gametime can be read (both in [ms]).

        :Variables:
            event_update : Event
                update event that is fired every time update is called,
                it passes gdt, the delta time passed in the game
    """

    def __init__(self, *args, **kwargs):
        """
        Init the GameTime with tick_speed=1.0.
        """
        super(GameTime, self).__init__(*args, **kwargs)
        self._factor = 1.0
        self._time = 0
        self._real_time = 0
        self.event_update = events.Signal("gametime event_update")

    def _set_factor(self, factor):
        """
        For use in the factor property only. You get a warning when setting a
        negative value, but it is permitted.

        :Parameters:
            factor : float
              time dilatation factor, 0.0 pauses, negative values run the time
              backwards (scheduling does not work when running backwards).
        """
        if __debug__:
            if 0 > factor:
                warnings.warn('Using negative time factor!!')
        self._factor = factor

    def _get_factor(self):
        """
        Returns the dilation factor.
        """
        return self._factor
    tick_speed = property(_get_factor, _set_factor, doc="""
        Set the dilatation factor, 0.0 pauses, negative raise a warning and let
        the time run backwards (scheduling does not work with negative values)
        """)

    def _get_time(self):
        """
        Returns the game time.
        """
        return self._time
    time = property(_get_time, doc=""" game time since start, read-only""")

    def _get_real_time(self):
        """
        Returns the real time.
        """
        return self._real_time
    real_time = property(_get_real_time, doc=""" real time since start, read-only""")

    def update(self, real_delta_t):
        """
        Should be called each frame to update the gametime.
        real_delta_t is the time passes in the last 'frame'.
        It fires the event_update with the arguments: gdt

            gdt : float
                game delta time

        :Parameters:
            real_delta_t : int
                real delta time (same as input)

        The gametime is only advanced if update is called.
        """
        self._real_time += real_delta_t
        gdt = self._factor * real_delta_t
        self._time += gdt
        # self.event_update.fire(self, gdt, self._time, dt, self._real_time)
        self.event_update.fire(gdt)

# ------------------------------------------------------------------------------



class LockStepper(object):
    """
    Based on: `<http://gafferongames.com/game-physics/fix-your-timestep/>`_

    This will advance its 'sim_time' in a 'lock step' mode.

    To register a integrate method of your simulation:

    >>> import timing
    >>> stepper = timing.LockStepper()
    >>> def integrate(delta_seconds, sim_time): pass
    ...
    >>> stepper.event_integrate += integrate
    >>>

    :param max_steps: max number of steps taken in this frame, needed for
        the undersampling case, this will slow down the simulation
    :type max_steps: int

    """

    TIMESTEP_SECONDS = 0.01


    def __init__(self, max_steps=8, max_dt=0.05):
        """
        """
        self._accumulator = 0.0
        self._sim_time = 0
        self.event_integrate = events.Signal()
        self.max_steps = max_steps
        self.max_dt = max_dt

    def update(self, dt_seconds, timestep_seconds=TIMESTEP_SECONDS):
        """
        Updates the simulation using a in descete time deltas (lock step).

        This triggers the 'event_integrate' signal

        :param dt_seconds: time in seconds passed since last frame
            make sure to clamp this value to a maximum value (because
            if a large value is passed in you don't want to wait until
            all steps have been calculated)
        :type dt_seconds: float
        :param timestep_seconds: the step size of the lock step
        :type timestep_seconds: float
        :returns: alpha in range [0, 1.0], is the remainder and means how
            far between the current step and the next step the simulation is.
            This should be used to interpolate the position of the current and
            previous state so the graphics are smoth.
        :rtype: float

        """
        # prevent spiral of death
        dt_seconds = self.max_dt if dt_seconds > self.max_dt else dt_seconds 
        
        self._accumulator += dt_seconds
        num_steps = 0
        while self._accumulator >= timestep_seconds and \
                                                num_steps < self.max_steps:
            self._sim_time += timestep_seconds
            self._accumulator -= timestep_seconds
            num_steps += 1
            self.event_integrate.fire(self, timestep_seconds, self._sim_time)

        if __debug__:
            # todo: maybe remove this, or log it... would be nice to know
            # about it dont know how to achieve it in a good way
            if num_steps >= self.max_steps:
                _LOGGER.info("LockStepper::update() took max_steps" + \
                                                            str(self.max_steps))
            if self._accumulator > 200 * timestep_seconds:
                _LOGGER.warn(\
                        "LockStepper::update() is way to sloooow to keep up!")

        alpha = self._accumulator / timestep_seconds
        return alpha - int(alpha) # because of max_steps alpha can be > 1.0

# ------------------------------------------------------------------------------

class Scheduler(object):
    """
    This is a scheduler that works on a frame basis. Call update() on each
    frame and all scheduled methods (== callbacks) will be called.

    The return value of the callback decides if the callback is re-scheduled.
    If a callback returns 0 the callback will be removed and no further
    re-scheduling is done (the callback is called once).

    To re-schedule the callback it should return a integer number describing
    the next frame it should be called back.

    ::

        def callback(*args, **kwargs):
            # code
            return 10 # this callback is called in 10 frames again

    """

    STOPREPEAT = 0.0

    def __init__(self):
        self._heap = []
        heapq.heapify(self._heap)
        self._nexttime = sys.maxsize
        self._current_time = 0
        self._current_frame = 0
        self._next_id = 999
        
    def _get_id(self):
        self._next_id += 1
        return self._next_id

    # TODO: add a 'schedule_once' method!
    def schedule(self, func, interval, offset=0, *args, **kwargs):
        """
        Schedule a function or method to be called back a time later.

        :param func: function or method to call back in the future
        :type func: function of method reference
        :param interval: time that should pass until next call of func,
            has to be >= 0 (a value of 0 means it is called next time
            the update method is called)
        :type interval: int (Scheduler) or float (Scheduler)
        :param offset: the start offset. This helps to distribute repeating
            callbacks evenly over time manually to prevent a high number
            of callbacks in a single frame.
        :type offset:  int (Scheduler) or float (Scheduler)
        :param args: arguments of the scheduled function
        :type args: list
        :param kwargs: keyword arguments of the scheduled function
        :type kwargs: dict

        .. todo:: using func to identify isnt enough because what if you schedule the same function multiple times and want to unschedule the last entry? use an ID to identify schedule??
        """
        assert interval >= 0
        id = self._get_id()
        next_time = self._current_time + interval + offset
        heappush(self._heap, (next_time, id, func, args, kwargs))
        if next_time < self._nexttime:
            self._nexttime = next_time
        return id

    def update(self, delta_time):
        """
        Call this every frame.

        :param delta_time: delta time passed, can be whatever time unit is used
            (even a frame count)
        :type delta_time: float, int
        """
        self._current_time += delta_time
        assert self._current_time < sys.maxsize # can this ever happend?
        heap = self._heap
        while self._nexttime <= self._current_time:
            next_time, id, func, args, kwargs = heappop(heap)

            interval = func(*args, **kwargs)

            if interval > 0.0:
                # current + intervall >= next_time + interval ==> drift of time of point of callback call !!
                heappush(heap, (next_time + interval, id, func, args, kwargs))

            if heap:
                self._nexttime = heap[0][0]
            else:
                # break
                self._nexttime = sys.maxsize

    def clear(self):
        """
        removes all callbacks.
        """
        self._heap[:] = []
        self._check_next()

    def remove(self, id):
        """
        Remove a callback function.

        :param id: the id of the function to remove, does nothing if id is not
            in schedule
        :type id: id returned by schedule
        """
        to_remove = []
        for entry in self._heap:
            next_time, entry_id, entry_func, args, kwargs = entry
            if entry_id == id:
                to_remove.append(entry)
                break
        for entry in to_remove:
            self._heap.remove(entry)
        self._check_next()

    def _check_next(self):
        """
        Checks when the next callback should be called.
        """
        if self._heap:
            self._nexttime = self._heap[0][0]
        else:
            self._nexttime = sys.maxsize

# ------------------------------------------------------------------------------

class Timer(object):
    """
    Timer is a convenience class to use a :class:`Scheduler` or one of its
    derived classes. By default it uses a :class:`Scheduler`. If
    another scheduler should be used, assign another instance of a
    Scheduler to Timer.scheduler *before* **any** use of the Timer class.

    Once a frame you need to call the schedulers update method:

    ::

        # once a frame in your mainloop
        Timer.scheduler.update(delta_time)

    You could assing different schedulers to instances of the Timer class
    by overriding the class attribute scheduler with an instance attribute

    ::

        my_scheduler = Scheduler()
        timer = Timer(10)
        timer.scheduler = my_scheduler

    Don't forget to update the used scheduler once per frame.

    The timer class has an event_elapsed attribute which is a
    :class:`events.Signal` instance. It is used like this:

    ::

        # my_callback should have following signature
        def my_callback(self, timer_that_fired):
            pass

        # register my_callback at the timer
        timer = Timer(0.5, False)
        timer.event_elapsed += my_callback



    :classattr scheduler: a Scheduler or a derived class

    :attr intervall: the intervall to be used
    :attr repeat: repeated firing of the elapsed event

    :param intervall: the time to wait until the event_elapsed is fired
    :type intervall: depends what the scheduler uses, defaults to a float
        representing seconds
    :param repeat: defaults to False, if the elapsed event should be fired
        repeatadly with the given intervall
    :type repeat: bool

    """

    scheduler = Scheduler()

    def __init__(self, intervall, repeat=False):
        """
        Constructor.
        """
        self.intervall = intervall
        self.repeat = repeat
        self.event_elapsed = events.Signal()
        self._is_running = False

    def start(self, repeat=None):
        """
        Starts the timer to fire the elapsed events (internally: it schedules
        a callback method to the scheduler).

        :param repeat: defaults to None, overwrites the repeat attribute if
            set to a boolean value
        :type repeat: bool
        """
        self._is_running = True
        self.scheduler.schedule(self._callback, self.intervall)
        if repeat is not None:
            self.repeat = repeat

    def stop(self):
        """
        Stops the timer from firing the elapsed event (internally: it removes
        all callbacks from the scheduler).
        """
        self._is_running = False
        self.scheduler.remove(self._callback, True)

    def _callback(self):
        """
        Callback method that gets called from the scheduler.
        """
        self.event_elapsed.fire(self)
        if self.repeat:
            return self.intervall
        else:
            return 0

    # def __del__(self):
        ## if Timer.scheduler would use weakrefs this would be useful
        # self.stop()
        # object.__del__(self)

# ------------------------------------------------------------------------------

# ------------------------------------------------------------------------------

if __debug__:
    _DELTA = time.time() - _START_TIME
    _LOGGER.debug('%s loaded: %fs \n' % (__name__, _DELTA))

